# -*- coding: utf-8 -*-
# $Id: udineGenUtils.py 6 2010-01-08 03:31:29Z leo.monash.uni $
import math, string, glob
from math import ceil
from random import seed, random, choice, randrange

class TimetablingInstance:
  "A class storing properties of a (randomly generated) instance of a course timetabling problem"

  def __init__(self, events, occupancy,nameConvention="Name: Random_%iEvents_Occupancy%i\n"):
    self.pEvents = events
    self.pOccupancy = float(occupancy)/100
    self.nameConvention = nameConvention

    self.pDays = 5
    self.pPeriodsPerDay = 5
    self.pPeriods = self.pDays * self.pPeriodsPerDay 

    self.pMinEventsPerCourse = 2
    self.pMaxEventsPerCourse = 5
    self.pCourses = 0            # reset in genCourses!
    self.pMinDaysFactor = 1.125
    self.pMaxRoomCourseRatio = 1
    self.pMinRoomCourseRatio = 0.7

    self.pTeachers = int(self.pEvents / 6)
    self.pMinCoursesPerTeacher = 1
    self.pMaxCoursesPerTeacher = 3

    self.pCurricula = int(self.pEvents / 10)
    self.pMinCoursesPerCurriculum = 2
    self.pMaxCoursesPerCurriculum = 6

    self.pRooms = max(int(ceil(self.pEvents / self.pPeriods / self.pOccupancy)), 1)
    self.pDistinctRoomSizes = max(int(ceil(0.6 * self.pRooms)), 1)
    self.pSmallestClassroom = 40
    self.pLargestClassroom = 350

    self.pConstraints = int(self.pEvents * 1.2)

    self.intRoomSizes = []
    self.intCourseIds = []

  def genRooms(self):
    outRooms = "\nROOMS:\n"
    distinct = []
    for i in range(self.pDistinctRoomSizes):
      distinct.append(randrange(self.pSmallestClassroom, self.pLargestClassroom, 5))
    complete = distinct[:]
    for j in range(self.pRooms - self.pDistinctRoomSizes):
      complete.append(choice(distinct))
    complete.sort()
    intRoomSizes = complete[:] 
    for room, capacity in zip(range(self.pRooms), complete):
      outRooms += "R%02i\t%i\n" % (room + 1, capacity)
    return outRooms
    
  def genCourses(self):
    outCourses = "\nCOURSES:\n"
    eventsCounts = []
    minDaysSpecs = []
    taughtEvents = 0
    while taughtEvents < self.pEvents:
      ev = min(randrange(self.pMinEventsPerCourse, self.pMaxEventsPerCourse), self.pEvents - taughtEvents)
      eventsCounts.append(ev)
      minDaysSpecs.append(int(ev/self.pMinDaysFactor))
      taughtEvents += ev
    self.pCourses = len(eventsCounts)
    taught = self.pMinCoursesPerTeacher * range(self.pTeachers)
    teachers = set(range(self.pTeachers))
    while len(taught) < self.pCourses:
      teacher = teachers.pop()
      for i in range(max(self.pMaxCoursesPerTeacher - self.pMinCoursesPerTeacher, len(taught) - self.pCourses)):
        taught.append(teacher)
    students = [randrange(int(self.pSmallestClassroom/2), self.pLargestClassroom) for j in range(self.pCourses)]
    for course, teacher, eventsCount, minDays, capacity in zip(range(self.pCourses), taught, eventsCounts, minDaysSpecs, students):
      id = "Course%03i" % (course + 1)
      self.intCourseIds.append(id)
      outCourses += "%s t%02i %i %i %i\n" % (id, teacher, eventsCount, minDays, capacity)
    return outCourses

  def genUnavailability(self):
    outUnavail = "\nUNAVAILABILITY_CONSTRAINTS:\n"
    unavail = []
    while len(unavail) < self.pConstraints:
      course = choice(self.intCourseIds)
      day = randrange(0, self.pDays)
      period = randrange(0, self.pPeriodsPerDay)
      if random() > 0.5:
        for p in range(period, self.pPeriodsPerDay): unavail.append((course, day, p))
      else:
        for p in range(0, period+1): unavail.append((course, day, p))
    unavail = sorted(list(set(unavail))) # May result in the number of constraints being cut, so...
    self.pConstraints = len(unavail) # ... update the number of constraints.
    for course, day, period in unavail:
      outUnavail += "%s %i %i\n" % (course, day, period)
    return outUnavail

  def genCurricula(self):
    outCurr = "\nCURRICULA:\n"
    curr = []
    for i in range(self.pCurricula):
      courses = []
      for coursei in range(self.pMinCoursesPerCurriculum):
        courses.append(choice(self.intCourseIds))
      if self.pMaxCoursesPerCurriculum - self.pMinCoursesPerCurriculum > 0:
        for coursei in range(randrange(1, self.pMaxCoursesPerCurriculum - self.pMinCoursesPerCurriculum, 1)):
          courses.append(choice(self.intCourseIds))
      courses = list(set(courses))
      courses.sort()
      curr.append((i, courses))
    for id, courses in curr:
      outCurr += "Curr%003i %i %s\n" % (id, len(courses), string.join(courses, " "))
    return outCurr

  def genHeader(self,name=None):
    if name: outHeader = "Name: %s\n" % name
    else: outHeader = self.nameConvention % (self.pEvents, int(self.pOccupancy * 100))
    outHeader += "Courses: %i\n" % self.pCourses
    outHeader += "Rooms: %i\n" % self.pRooms
    outHeader += "Days: %i\n" % self.pDays
    outHeader += "Periods_per_day: %i\n" % self.pPeriodsPerDay
    outHeader += "Curricula: %i\n" % self.pCurricula
    outHeader += "Constraints: %i\n" % self.pConstraints
    return outHeader

  def genInstanceDef(self,name=None):
    # seed(newSeed) # don't randomize, let user pick seed.
    rooms = self.genRooms()
    courses = self.genCourses()
    curr = self.genCurricula()
    unavail = self.genUnavailability()
    header = self.genHeader(name)
    return header + courses + rooms + curr + unavail + "\nEND."

if __name__ == '__main__':
  import sys
  if len(sys.argv) < 3:
    print "Usage: %s <nEvents> <nOccupancy>" % argv[0]
    sys.exit(1)
  print TimetablingInstance(int(sys.argv[1]),int(sys.argv[2])).genInstanceDef()

